package org.apache.cordova.plugin;

import android.net.Uri;
import android.util.Log;
import android.content.Context;
import android.content.ContentResolver;

import androidx.documentfile.provider.DocumentFile;

import org.apache.cordova.PluginResult;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.UUID;
import java.net.URLConnection;

import fi.iki.elonen.NanoHTTPD;

public class NanoHTTPDWebserver extends NanoHTTPD {

    Webserver webserver;
    Context context;

    public NanoHTTPDWebserver(int port, Webserver webserver, Context context) {
        super(port);
        this.webserver = webserver;
        this.context = context;
    }

    private String getBodyText(IHTTPSession session) {
        Map<String, String> files = new HashMap<String, String>();
        Method method = session.getMethod();
        if (Method.PUT.equals(method) || Method.POST.equals(method)) {
            try {
                session.parseBody(files);
            } catch (IOException ioe) {
                return "{}";
            } catch (ResponseException re) {
                return "{}";
            }
        }
        // get the POST body
        return files.get("postData");
    }

    /**
     * Create a request object
     * <p>
     * [ "requestId": requestUUID, " body": request.jsonObject ?? "", " headers":
     * request.headers, " method": request.method, " path": request.url.path, "
     * query": request.url.query ?? "" ]
     *
     * @param session
     * @return
     */
    private JSONObject createJSONRequest(String requestId, IHTTPSession session) throws JSONException {
        JSONObject jsonRequest = new JSONObject();
        jsonRequest.put("requestId", requestId);
        jsonRequest.put("body", this.getBodyText(session));
        jsonRequest.put("headers", session.getHeaders());
        jsonRequest.put("method", session.getMethod());
        jsonRequest.put("path", session.getUri());
        jsonRequest.put("query", session.getQueryParameterString());
        return jsonRequest;
    }

    private String getContentType(JSONObject responseObject) throws JSONException {
        if (responseObject.has("headers") && responseObject.getJSONObject("headers").has("Content-Type")) {
            return responseObject.getJSONObject("headers").getString("Content-Type");
        } else {
            return "text/plain";
        }
    }

    private Response newFixedFileResponse(DocumentFile file, String mime) throws FileNotFoundException, IOException {
        Response res;
        res = newFixedLengthResponse(Response.Status.OK, mime, getInputStream(file), (int) file.length());
        res.addHeader("Accept-Ranges", "bytes");
        return res;
    }

    Response serveFile(Map<String, String> header, DocumentFile file, String mime) {
        Response res;
        try {
            // Calculate etag
            long fileLen = file.length();
            String path = file.getUri().toString();
            String etag = Integer.toHexString((path + file.lastModified() + "" + fileLen).hashCode());

            // Support (simple) skipping:
            long startFrom = 0;
            long endAt = -1;
            String range = header.get("range");
            if (range != null) {
                if (range.startsWith("bytes=")) {
                    range = range.substring("bytes=".length());
                    int minus = range.indexOf('-');
                    try {
                        if (minus > 0) {
                            startFrom = Long.parseLong(range.substring(0, minus));
                            endAt = Long.parseLong(range.substring(minus + 1));
                        }
                    } catch (NumberFormatException ignored) {
                    }
                }
            }

            // get if-range header. If present, it must match etag or else we
            // should ignore the range request
            String ifRange = header.get("if-range");
            boolean headerIfRangeMissingOrMatching = (ifRange == null || etag.equals(ifRange));

            String ifNoneMatch = header.get("if-none-match");
            boolean headerIfNoneMatchPresentAndMatching = ifNoneMatch != null
                    && ("*".equals(ifNoneMatch) || ifNoneMatch.equals(etag));

            if (headerIfRangeMissingOrMatching && range != null && startFrom >= 0 && startFrom < fileLen) {
                // range request that matches current etag
                // and the startFrom of the range is satisfiable
                if (headerIfNoneMatchPresentAndMatching) {
                    // range request that matches current etag
                    // and the startFrom of the range is satisfiable
                    // would return range from file
                    // respond with not-modified
                    res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
                    res.addHeader("ETag", etag);
                } else {
                    if (endAt < 0) {
                        endAt = fileLen - 1;
                    }
                    long newLen = endAt - startFrom + 1;
                    if (newLen < 0) {
                        newLen = 0;
                    }

                    InputStream is = getInputStream(file);
                    is.skip(startFrom);

                    res = newFixedLengthResponse(Response.Status.PARTIAL_CONTENT, mime, is, newLen);
                    res.addHeader("Accept-Ranges", "bytes");
                    res.addHeader("Content-Length", "" + newLen);
                    res.addHeader("Content-Range", "bytes " + startFrom + "-" + endAt + "/" + fileLen);
                    res.addHeader("ETag", etag);
                }
            } else {

                if (headerIfRangeMissingOrMatching && range != null && startFrom >= fileLen) {
                    // return the size of the file
                    // 4xx responses are not trumped by if-none-match
                    res = newFixedLengthResponse(Response.Status.RANGE_NOT_SATISFIABLE, NanoHTTPD.MIME_PLAINTEXT, "");
                    res.addHeader("Content-Range", "bytes */" + fileLen);
                    res.addHeader("ETag", etag);
                } else if (range == null && headerIfNoneMatchPresentAndMatching) {
                    // full-file-fetch request
                    // would return entire file
                    // respond with not-modified
                    res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
                    res.addHeader("ETag", etag);
                } else if (!headerIfRangeMissingOrMatching && headerIfNoneMatchPresentAndMatching) {
                    // range request that doesn't match current etag
                    // would return entire (different) file
                    // respond with not-modified

                    res = newFixedLengthResponse(Response.Status.NOT_MODIFIED, mime, "");
                    res.addHeader("ETag", etag);
                } else {
                    // supply the file
                    res = newFixedFileResponse(file, mime);
                    res.addHeader("Content-Length", "" + fileLen);
                    res.addHeader("ETag", etag);
                }
            }
        } catch (Exception e) {
            String type = e.getClass().getName();
            if (type.equals("FileNotFoundException"))
                res = newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, e.getMessage());
            else
                res = newFixedLengthResponse(Response.Status.FORBIDDEN, NanoHTTPD.MIME_PLAINTEXT, e.getMessage());
        }

        return res;
    }

    @Override
    public Response serve(IHTTPSession session) {
        Log.d(this.getClass().getName(), "New request is incoming!");

        String requestUUID = UUID.randomUUID().toString();

        PluginResult pluginResult = null;
        try {
            pluginResult = new PluginResult(PluginResult.Status.OK, this.createJSONRequest(requestUUID, session));
        } catch (JSONException e) {
            e.printStackTrace();
        }
        pluginResult.setKeepCallback(true);
        this.webserver.onRequestCallbackContext.sendPluginResult(pluginResult);

        while (!this.webserver.responses.containsKey(requestUUID)) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        JSONObject responseObject = (JSONObject) this.webserver.responses.get(requestUUID);
        Response response = null;
        // Log.d(this.getClass().getName(), "responseObject: " +
        // responseObject.toString());

        if (responseObject.has("path")) {
            try {
                String path = responseObject.getString("path");
                DocumentFile file = getFile(path);
                String mimeType = URLConnection.guessContentTypeFromName(path);
                Response res = serveFile(session.getHeaders(), file, mimeType);
                Iterator<?> keys = responseObject.getJSONObject("headers").keys();
                while (keys.hasNext()) {
                    String key = (String) keys.next();
                    res.addHeader(key, responseObject.getJSONObject("headers").getString(key));
                }
                return res;
            } catch (JSONException e) {
                e.printStackTrace();
            }
            return response;
        } else {
            try {
                response = newFixedLengthResponse(Response.Status.lookup(responseObject.getInt("status")),
                        getContentType(responseObject), responseObject.getString("body"));

                Iterator<?> keys = responseObject.getJSONObject("headers").keys();
                while (keys.hasNext()) {
                    String key = (String) keys.next();
                    response.addHeader(key, responseObject.getJSONObject("headers").getString(key));
                }

            } catch (JSONException e) {
                e.printStackTrace();
            }
            return response;
        }
    }

    private DocumentFile getFile(String filePath) {
        Uri fileUri = Uri.parse(filePath);
        DocumentFile documentFile = null;
        if (filePath.matches("file:///(.*)")) {
            File file = new File(fileUri.getPath());
            documentFile = DocumentFile.fromFile(file);
        } else {
            documentFile = DocumentFile.fromSingleUri(this.context, Uri.parse(filePath));
        }

        return documentFile;
    }

    private InputStream getInputStream(DocumentFile file) throws FileNotFoundException, IOException {
        Uri uri = file.getUri();
        ContentResolver contentResolver = context.getContentResolver();
        return contentResolver.openInputStream(uri);
    }
}
